// Copyright(c) 2015-present, Gabi Melman & spdlog contributors.
// Distributed under the MIT License (http://opensource.org/licenses/MIT)

#pragma once

#ifndef SPDLOG_HEADER_ONLY
    #include <spdlog/sinks/backup_rotating_file_sink.h>
#endif

#include <spdlog/common.h>

#include <spdlog/details/file_helper.h>
#include <spdlog/details/null_mutex.h>
#include <spdlog/fmt/fmt.h>

#include <cerrno>
#include <ctime>
#include <mutex>
#include <string>
#include <tuple>

#include <type_traits>

namespace spdlog {
namespace sinks {

template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE backup_rotating_file_sink<Mutex, ArchivePtrType>::backup_rotating_file_sink(
    filename_t base_filename,
    std::size_t max_size,
    std::size_t max_files,
    ArchivePtrType parchiver,
    bool rotate_on_open,
    const file_event_handlers& event_handlers)
    : base_filename_(std::move(base_filename)),
      max_size_(max_size),
      max_files_(max_files),
      parchiver_(std::move(parchiver)),
      file_helper_{event_handlers}
{
    if (max_size == 0) {
        throw_spdlog_ex("rotating sink constructor: max_size arg cannot be zero");
    }

    if (max_files > MaxFiles) {
        throw_spdlog_ex("rotating sink constructor: max_files arg cannot exceed MaxFiles");
    }
    
    file_helper_.open(calc_filename(base_filename_, 0));
    
    /*
    Pre-split the file path and file extension, instead of splitting in real-time with
    calc_filename, to minimize runtime execution overhead as much as possible.
    */
    std::tie<std::string, std::string>(dir_filename, ext) = spdlog::details::file_helper::split_by_extension(base_filename_);

    current_size_ = file_helper_.size();  // expensive. called only once
    if (rotate_on_open && current_size_ > 0) 
    {
        rotate_();
        current_size_ = 0;
    }
}


template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE backup_rotating_file_sink<Mutex, ArchivePtrType>::~backup_rotating_file_sink() 
{
    //printf("backup_rotating_file_sink destructor called.\n");
}

// calc filename according to index and file extension if exists.
// e.g. calc_filename("logs/mylog.txt, 3) => "logs/mylog.3.txt".
template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE filename_t backup_rotating_file_sink<Mutex, ArchivePtrType>::calc_filename(const filename_t& filename, std::size_t index) 
{
    if (index == 0U) 
    {
        return filename;
    }

    return fmt_lib::format(SPDLOG_FMT_STRING(SPDLOG_FILENAME_T("{}.{}{}")), dir_filename, index, ext);
}

template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE filename_t backup_rotating_file_sink<Mutex, ArchivePtrType>::filename() {
    std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
    return file_helper_.filename();
}

template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE void backup_rotating_file_sink<Mutex, ArchivePtrType>::rotate_now() {
    std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
    rotate_();
}
template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE void backup_rotating_file_sink<Mutex, ArchivePtrType>::set_max_size(std::size_t max_size) {
    std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
    if (max_size == 0) {
        throw_spdlog_ex("rotating sink set_max_size: max_size arg cannot be zero");
    }
    max_size_ = max_size;
}

template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE std::size_t backup_rotating_file_sink<Mutex, ArchivePtrType>::get_max_size() {
    std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
    return max_size_;
}

template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE void backup_rotating_file_sink<Mutex, ArchivePtrType>::set_max_files(std::size_t max_files) {
    std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
    if (max_files > MaxFiles) {
        throw_spdlog_ex("rotating sink set_max_files: max_files arg cannot exceed 200000");
    }
    max_files_ = max_files;
}

template <typename Mutex, typename ArchivePtrType>
std::size_t backup_rotating_file_sink<Mutex, ArchivePtrType>::get_max_files() {
    std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
    return max_files_;
}

template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE void backup_rotating_file_sink<Mutex, ArchivePtrType>::sink_it_(const details::log_msg& msg) {
    memory_buf_t formatted;
    base_sink<Mutex>::formatter_->format(msg, formatted);
    auto new_size = current_size_ + formatted.size();

    // rotate if the new estimated file size exceeds max size.
    // rotate only if the real size > 0 to better deal with full disk (see issue #2261).
    // we only check the real size when new_size > max_size_ because it is relatively expensive.
    if (new_size > max_size_) {
        file_helper_.flush();
        if (file_helper_.size() > 0) {
            rotate_();
            new_size = formatted.size();
        }
    }
    file_helper_.write(formatted);
    current_size_ = new_size;
}

template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE void backup_rotating_file_sink<Mutex, ArchivePtrType>::flush_() {
    file_helper_.flush();
}

// Rotate files:
// log.txt -> log.1.txt
// log.1.txt -> log.2.txt
// log.2.txt -> log.3.txt
// log.3.txt -> backup to log.zip -> delete log.3.txt
template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE void backup_rotating_file_sink<Mutex, ArchivePtrType>::rotate_() {
    using details::os::filename_to_str;
    using details::os::path_exists;

    file_helper_.close();
    for (auto i = max_files_; i > 0; --i) {
        filename_t src = calc_filename(base_filename_, i - 1);
        if (!path_exists(src)) {
            continue;
        }
        filename_t target = calc_filename(base_filename_, i);

        if (!rename_file_(src, target, i)) {
            // if failed try again after a small delay.
            // this is a workaround to a windows issue, where very high rotation
            // rates can cause the rename to fail with permission denied (because of antivirus?).
            details::os::sleep_for_millis(100);
            if (!rename_file_(src, target, i)) {
                file_helper_.reopen(
                    true);  // truncate the log file anyway to prevent it to grow beyond its limit!
                current_size_ = 0;
                throw_spdlog_ex("rotating_file_sink: failed renaming " + filename_to_str(src) +
                                    " to " + filename_to_str(target),
                                errno);
            }
        }
    }
    file_helper_.reopen(true);
}

// delete the target if exists, and rename the src file  to target
// return true on success, false otherwise.
template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE bool backup_rotating_file_sink<Mutex, ArchivePtrType>::rename_file_(const filename_t& src_filename, const filename_t& target_filename, const std::size_t index) 
{
    /*
    In order to avoid affecting the operation of the logging functionality, even if the backup
    fails, no exception should be thrown. Therefore, we simply output the error message to the
    stderr stream. or execute a user-defined error handler.
    */
   using details::os::path_exists;
   if(parchiver_ && index == max_files_ && path_exists(target_filename))
    {
        backup_file(target_filename);
    }

    // try to delete the target file in case it already exists.
    (void)details::os::remove(target_filename);
    return details::os::rename(src_filename, target_filename) == 0;
}


/*
Since it's created based on a synchronous factory, the entire logging system relies on synchronous operations. If a separate thread is used to handle ZIP compression, it would break the existing design patterns and usage rules.

In this case, the current implementation approach is reasonable. Although synchronous processing of ZIP compression will block the logging thread, it maintains consistency with the overall architecture of spdlog. If users need fully asynchronous logging processing, they can choose to use spdlog's asynchronous logging functionality.
*/
template <typename Mutex, typename ArchivePtrType>
SPDLOG_INLINE void backup_rotating_file_sink<Mutex, ArchivePtrType>::backup_file(const filename_t& target_filename)
{
    std::lock_guard<Mutex> lock(backup_mutex_);
    parchiver_->archive_file(target_filename);
}

}  // namespace sinks
}  // namespace spdlog
